﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Text.RegularExpressions;

namespace NetAcademia.Web.Mvc
{
    public class HtmlLaundry
    {
        private XDocument whitelist;

        public HtmlLaundry(string whitelistXml)
        {
            this.whitelist = XDocument.Parse(whitelistXml);
        }

        public HtmlLaundry(XDocument whitelist)
        {
            this.whitelist = whitelist;
        }


        public string CleanHtml(string html)
        {
            //1. make xml from html
            XDocument xhtml = Xmlize(html);
            
            //2. clean
            xhtml.Clean(whitelist.Root.Elements());

            //3. back to string

            string cleanedHtml;
            if (xhtml.Root.IsEmpty)
            {
                cleanedHtml = "";
            }
            else
            {
                cleanedHtml = xhtml.ToString();
                int index = cleanedHtml.IndexOf('>');
                cleanedHtml = cleanedHtml.Substring(index + 1);
                index = cleanedHtml.LastIndexOf('<');
                cleanedHtml = cleanedHtml.Substring(0, index);               
            }
            return cleanedHtml.Trim();
        }


        #region convert to xml

        public XDocument Xmlize(string source)
        {
            //itt a regexet a fentebb megadott határoló karakterekkel behelyettesítem
            //ez három regex
            //az első a nyitott tag keresésére szolgál, mint pl. a <table> vagy a <span style="color:red" class=hopp id='valami'>   <\s*(?<tag>\w+)\s*(?<attribs>(\w*\s*=\s*(("[^"]*")|('[^']*')|(\w*))\s*)*)>
            //a második az egyből bezárt elemekt keresi, mint pl. a <br /> (akár attribútumokkal is)  <\s*(?<tag>\w+)\s*(?<attribs>(\w*\s*=\s*(("[^"]*")|('[^']*')|(\w*))\s*)*)/\s*>
            //a harmadik a záró elemekt keresi mint pl. a </table> (ezekben m'r nem lehet attribútum) <\s*/\s*(?<tag>\w+)\s*>
            //Regex exp = new Regex(@"(?<start><((?<namespace>\w+):)?(?<tag>[a-zA-Z0-9]+)\s*(?<attribs>.*?)(?<selfclosed>/?)\s*>)|(?<end><\s*/\s*((?<namespace>\w+):)?(?<tag>[a-zA-Z0-9]+)\s*>)|(?<comment><[!?].*?>)", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            Regex exp = new Regex(@"(?<start><((?<namespace>\w+):)?(?<tag>[a-zA-Z]+[a-zA-Z0-9]*)\s*(?<attribs>.*?)(?<selfclosed>/?)\s*>)|(?<end><\s*/\s*((?<namespace>\w+):)?(?<tag>[a-zA-Z0-9]+[a-zA-Z0-9]*)\s*>)|(?<comment><[!?].*?>)", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

            //régi ami nem fogta meg az aposztrófos attribútumokat és az idézőjel nélkülieket
            //Regex exp = new Regex(string.Format(@"(?<start>{0}\s*(?<tag>\w+)\s*(?<attribs>(\w*\s*=\s*""[^""]*""\s*)*){1})|(?<selfclosed>{0}\s*(?<tag>\w+)\s*(?<attribs>(\w*\s*=\s*""[^""]*""\s*)*)\s*/\s*{1})|(?<end>{0}\s*/\s*(?<tag>\w+)\s*{1})", OpenTagCharRegEx, CloseTagCharRegEx));

            XDocument dom = new XDocument();
            //dom.Add(new XComment(source));
            // dom.Add(new XComment("Az xml formátumra hozott dokumentum nem feltétlen tartalmaz minden információt az eredeti forrásból, és amit tartalmaz, abban is lehet whitespace eltérés. Tocsi"));
            XElement root = new XElement("root");
            dom.Add(root);

            Match m = exp.Match(source);
            int pos = 0; //ez lesz mindig az előző match vége

            string currentText;
            XElement xCurrent = root;
            XElement xNew;

            while (m.Success)
            {
                currentText = source.Substring(pos, m.Index - pos);  //a kát találat közötti szöveg
                pos = m.Index + m.Length; //pointert a találat utánra állítom

                if (!string.IsNullOrEmpty(currentText)) //ha a két találat között tényleg volt valami
                {
                    currentText = System.Web.HttpUtility.HtmlDecode(currentText);
                    xCurrent.Add(currentText); //akkor azt a szöveget, az aktuális node-ba rakom
                }

                if (m.Groups["comment"].Success)
                {
                    //commenteket kihagyjuk
                }
                else
                {
                    string tagname = m.Groups["tag"].Value.ToLower();
                    string prefix = m.Groups["namespace"].Value.ToLower();

                    var elementName = XName.Get(tagname, prefix);

                    if (m.Groups["start"].Success) //ha ez egy kezdő tag, vagy egy magába zárkózott
                    {
                        //ha volt valami prefix, akkor azt névtérként bejegyzem
                        if (!string.IsNullOrWhiteSpace(prefix))
                        {
                            var nsname = XName.Get(prefix, "http://www.w3.org/2000/xmlns/");
                            dom.Root.SetAttributeValue(nsname, prefix);
                        }
                       
                        xNew = new XElement(elementName); //a találat nevével új elemet hozok létre
                        

                        UnwashedDirts problem = AddAttributes(xNew, m.Groups["attribs"].Value);
                        //attribútumok után kutatok
                        if (problem != UnwashedDirts.NoProblem) //ha gond volt, akkor dobjunk egy eseményt
                        {
                            OnXmlizerLazinessOccured(new LazinessOccuredEventArgs
                            {
                                Problem = problem,
                                SourcePosition = pos,
                                SourceSegment = m.Groups["attribs"].Value
                            });
                        }

                        xCurrent.Add(xNew);
                        //de csak akkor lesz ez az új aktuális node, ha nem magába zárkózott !m.Groups["selfclosed"].Success || 
                        if (
                            !(!string.IsNullOrEmpty(m.Groups["selfclosed"].Value.Trim()) ||
                              TagsWithoutClosingElement.Contains(tagname)))
                            xCurrent = xNew;
                    }
                    else //egyébként egy vége tag volt
                    {
                        UnwashedDirts problem = UnwashedDirts.NoProblem;

                        //van-e a láncon (az XCurrentet is beleértve) olyan nevű mint ez a tagName?
                        //var openElement = xCurrent.AncestorsAndSelf().FirstOrDefault(
                        //        n => (ToLowerTags ? n.Name.ToString().ToLower() : n.Name) == tagname);

                        var openElement = xCurrent.AncestorsAndSelf().FirstOrDefault(n => n.Name == elementName);

                        if (openElement != null)
                        {
                            //akkor amíg vele azonos node-ot nem találunk, mindent be kell zárni
                            //while (tagname != (ToLowerTags ? xCurrent.Name.ToString().ToLower() : xCurrent.Name))
                            //{
                            //    xCurrent = xCurrent.Parent;
                            //    problem = UnwashedDirts.OverlappedClosingTag;
                            //}
                            while (elementName != xCurrent.Name)
                            {
                                xCurrent = xCurrent.Parent;
                                problem = UnwashedDirts.OverlappedClosingTag;
                            }
                            xCurrent = xCurrent.Parent; //és végül a vele azonosat is lezárom
                        }
                        else
                        {
                            problem = UnwashedDirts.NoMatchingOverlappedClosingTag;
                        }

                        if (problem != UnwashedDirts.NoProblem)
                        {
                            OnXmlizerLazinessOccured(new LazinessOccuredEventArgs
                            {
                                Problem = problem,
                                SourcePosition = pos,
                                SourceSegment = m.ToString()
                            });
                        }
                    }
                }
                m = m.NextMatch();
            }

            //és még a végén ha maradt szöveg, az is megy a rootba
            currentText = source.Substring(pos, source.Length - pos);
            if (!string.IsNullOrEmpty(currentText)) //ha a két találat között tényleg volt valami
            {
                currentText = System.Web.HttpUtility.HtmlDecode(currentText);
                xCurrent.Add(currentText); //akkor azt a szöveget, az aktuális node-ba rakom
            }
            return dom;
        }



        /// <summary>
        /// Egy attribútum szöveget attribútumokként egy XElement-hez hozzáad
        /// </summary>
        /// <param elementName="xe">Amihez hozzá kell adni az attribútumokat</param>
        /// <param elementName="attributes">A szövegként tárol attribútum lista</param>
        /// <returns>
        /// Ha duplikált attribútumot talált, akkor UnwashedDirts.DuplicateAtribute,
        /// egyébként UnwashedDirts.NoProblem
        /// </returns>
        /// <example>
        /// Ha a 'barack="alma" dinnye="citrom"' szöveg a bement, akkor 
        /// abból két XAttribute fog keletkezni barack és dinnye néven, alma és citrom értékekkel
        /// </example>
        /// <remarks>
        /// Több azonos nevű attribútum nem lehet. Ha ilyen lenne a forrásban, akkor az első nyer.
        /// </remarks>
        private static UnwashedDirts AddAttributes(XElement xe, string attributes)
        {
            UnwashedDirts problem = UnwashedDirts.NoProblem;
            //Regex exp = new Regex(@"(w+:)?(?<attrib>\w+)\s*=\s*((""(?<value>[^""]*))|('(?<value>[^']*))|((?<value>\w*)))", RegexOptions.Singleline);
            Regex exp = new Regex(@"(w+:)?(?<attrib>\w+)\s*=\s*((""(?<value>[^""]*))|('(?<value>[^']*))|((?<value>\S*)))", RegexOptions.Singleline);

            //régi ami nem fogta meg az aposztrófos attribútumokat és az idézőjel nélkülieket
            //Regex exp = new Regex(@"(?<attrib>\w*)\s*=\s*""(?<value>[^""]*)""");

            MatchCollection mc = exp.Matches(attributes);
            if (mc.Count > 0)
            {

                foreach (Match match in mc)
                {
                    string attributeName = match.Groups["attrib"].Value;
                    if (attributeName == "xmlns")
                        problem = UnwashedDirts.XmlNamespacesNotSupported;
                    else if (xe.Attribute(attributeName) == null)
                        xe.Add(new XAttribute(attributeName, match.Groups["value"].Value));
                    else
                        problem = UnwashedDirts.DuplicateAtribute;
                }
            }
            return problem;
        }

        private List<string> tagsWithoutClosingElement;

        protected virtual List<string> TagsWithoutClosingElement
        {
            get
            {
                if (tagsWithoutClosingElement == null)
                    tagsWithoutClosingElement = new List<string>() { "br", "param", "hr", "input", "col", "link", "meta" };


                return tagsWithoutClosingElement;
            }
        }


        public event LazinessOccuredEventHandler XmlizerLazinessOccured;

        /// <summary>
        /// Ez dobja a azokat az eseményeket, amik valami inkorrekt xml forrással vannak kapcsolatban
        /// </summary>
        /// <param elementName="args"></param>
        protected virtual void OnXmlizerLazinessOccured(LazinessOccuredEventArgs args)
        {
            if (XmlizerLazinessOccured != null)
                XmlizerLazinessOccured(this, args);
        }

        #endregion
    }
}
